/*! ******************************************************************************
 *
 * Pentaho
 *
 * Copyright (C) 2024 by Hitachi Vantara, LLC : http://www.pentaho.com
 *
 * Use of this software is governed by the Business Source License included
 * in the LICENSE.TXT file.
 *
 * Change Date: 2029-07-20
 ******************************************************************************/


package org.pentaho.platform.security.userroledao.jackrabbit;

import com.google.common.annotations.VisibleForTesting;
import org.apache.commons.collections.map.LRUMap;
import org.apache.jackrabbit.api.security.user.Authorizable;
import org.apache.jackrabbit.api.security.user.AuthorizableExistsException;
import org.apache.jackrabbit.api.security.user.Group;
import org.apache.jackrabbit.api.security.user.User;
import org.apache.jackrabbit.api.security.user.UserManager;
import org.apache.jackrabbit.core.SessionImpl;
import org.apache.jackrabbit.core.security.authentication.CryptedSimpleCredentials;
import org.apache.jackrabbit.core.security.principal.PrincipalImpl;
import org.apache.jackrabbit.core.security.user.PentahoUserManagerImpl;
import org.apache.jackrabbit.spi.Name;
import org.apache.jackrabbit.spi.NameFactory;
import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl;
import org.pentaho.platform.api.engine.ISystemConfig;
import org.pentaho.platform.api.engine.security.userroledao.IPentahoRole;
import org.pentaho.platform.api.engine.security.userroledao.IPentahoUser;
import org.pentaho.platform.api.engine.security.userroledao.IUserRoleDao;
import org.pentaho.platform.api.engine.security.userroledao.NotFoundException;
import org.pentaho.platform.api.mt.ITenant;
import org.pentaho.platform.api.mt.ITenantedPrincipleNameResolver;
import org.pentaho.platform.api.repository2.unified.IRepositoryDefaultAclHandler;
import org.pentaho.platform.api.repository2.unified.RepositoryFile;
import org.pentaho.platform.api.repository2.unified.RepositoryFileAcl;
import org.pentaho.platform.api.repository2.unified.RepositoryFileAcl.Builder;
import org.pentaho.platform.api.repository2.unified.RepositoryFilePermission;
import org.pentaho.platform.api.repository2.unified.RepositoryFileSid;
import org.pentaho.platform.api.repository2.unified.RepositoryFileSid.Type;
import org.pentaho.platform.engine.core.system.PentahoSessionHolder;
import org.pentaho.platform.engine.core.system.PentahoSystem;
import org.pentaho.platform.engine.core.system.TenantUtils;
import org.pentaho.platform.repository2.unified.IRepositoryFileAclDao;
import org.pentaho.platform.repository2.unified.IRepositoryFileDao;
import org.pentaho.platform.repository2.unified.ServerRepositoryPaths;
import org.pentaho.platform.repository2.unified.jcr.ILockHelper;
import org.pentaho.platform.repository2.unified.jcr.IPathConversionHelper;
import org.pentaho.platform.repository2.unified.jcr.JcrRepositoryFileAclUtils;
import org.pentaho.platform.repository2.unified.jcr.JcrRepositoryFileUtils;
import org.pentaho.platform.repository2.unified.jcr.JcrTenantUtils;
import org.pentaho.platform.repository2.unified.jcr.PentahoJcrConstants;
import org.pentaho.platform.repository2.unified.jcr.sejcr.CredentialsStrategySessionFactory;
import org.pentaho.platform.security.userroledao.PentahoRole;
import org.pentaho.platform.security.userroledao.PentahoUser;
import org.pentaho.platform.security.userroledao.messages.Messages;
import org.springframework.security.core.userdetails.UserCache;
import org.springframework.security.core.userdetails.cache.NullUserCache;

import javax.jcr.Credentials;
import javax.jcr.NamespaceException;
import javax.jcr.Node;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.Value;
import java.io.Serializable;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;
import java.util.Set;

public abstract class AbstractJcrBackedUserRoleDao implements IUserRoleDao {

  static NameFactory NF = NameFactoryImpl.getInstance();

  static Name P_PRINCIPAL_NAME = NF.create( Name.NS_REP_URI, "principalName" ); //$NON-NLS-1$

  protected ITenantedPrincipleNameResolver tenantedUserNameUtils;

  protected ITenantedPrincipleNameResolver tenantedRoleNameUtils;

  String pPrincipalName = "rep:principalName"; //$NON-NLS-1$

  IRepositoryFileAclDao repositoryFileAclDao;

  IRepositoryFileDao repositoryFileDao;

  String defaultTenant;

  String authenticatedRoleName;

  String tenantAdminRoleName;

  String repositoryAdminUsername;

  IPathConversionHelper pathConversionHelper;

  IRepositoryDefaultAclHandler defaultAclHandler;

  ILockHelper lockHelper;

  List<String> systemRoles;

  List<String> extraRoles;

  HashMap<String, PentahoUserManagerImpl> userMgrMap = new HashMap<String, PentahoUserManagerImpl>();

  private LRUMap userCache = new LRUMap( 4096 );

  private UserCache userDetailsCache = new NullUserCache();

  private boolean useJackrabbitUserCache = true;

  public AbstractJcrBackedUserRoleDao( ITenantedPrincipleNameResolver userNameUtils,
                                       ITenantedPrincipleNameResolver roleNameUtils, String authenticatedRoleName,
                                       String tenantAdminRoleName,
                                       String repositoryAdminUsername, IRepositoryFileAclDao repositoryFileAclDao,
                                       IRepositoryFileDao repositoryFileDao,
                                       final IPathConversionHelper pathConversionHelper, final ILockHelper lockHelper,
                                       final IRepositoryDefaultAclHandler defaultAclHandler,
                                       final List<String> systemRoles,
                                       final List<String> extraRoles, UserCache userDetailsCache )
      throws NamespaceException {
    this.tenantedUserNameUtils = userNameUtils;
    this.tenantedRoleNameUtils = roleNameUtils;
    this.authenticatedRoleName = authenticatedRoleName;
    this.tenantAdminRoleName = tenantAdminRoleName;
    this.repositoryAdminUsername = repositoryAdminUsername;
    this.repositoryFileAclDao = repositoryFileAclDao;
    this.repositoryFileDao = repositoryFileDao;
    this.pathConversionHelper = pathConversionHelper;
    this.lockHelper = lockHelper;
    this.defaultAclHandler = defaultAclHandler;
    this.systemRoles = systemRoles;
    this.extraRoles = extraRoles;
    this.userDetailsCache = userDetailsCache;
  }

  public void setRoleMembers( Session session, final ITenant theTenant, final String roleName,
                              final String[] memberUserNames ) throws RepositoryException, NotFoundException {
    List<IPentahoUser> currentRoleMembers = getRoleMembers( session, theTenant, roleName );
    String[] usersToBeRemoved = findRemovedUsers( currentRoleMembers, memberUserNames );

    // If we are unassigning a user or users from the Administrator role, we need to check if this is a logged in user
    // or a user designated as a system user. If it is then we
    // will display a message to the user.
    if ( ( oneOfUserIsMySelf( usersToBeRemoved ) || oneOfUserIsDefaultAdminUser( usersToBeRemoved ) )
        && tenantAdminRoleName.equals( roleName ) ) {
      throw new RepositoryException( Messages.getInstance().getString(
          "AbstractJcrBackedUserRoleDao.ERROR_0009_USER_REMOVE_FAILED_YOURSELF_OR_DEFAULT_ADMIN_USER" ) );
    }

    // If this is the last user from the Administrator role, we will not let the user remove.
    if ( tenantAdminRoleName.equals( roleName ) && ( currentRoleMembers != null && currentRoleMembers.size() > 0 )
        && memberUserNames.length == 0 ) {
      throw new RepositoryException( Messages.getInstance().getString(
          "AbstractJcrBackedUserRoleDao.ERROR_0001_LAST_ADMIN_ROLE", tenantAdminRoleName ) );
    }
    Group jackrabbitGroup = getJackrabbitGroup( theTenant, roleName, session );

    if ( ( jackrabbitGroup == null )
        || !TenantUtils.isAccessibleTenant( theTenant == null ? tenantedRoleNameUtils.getTenant( jackrabbitGroup
        .getID() ) : theTenant ) ) {
      throw new NotFoundException( Messages.getInstance().getString(
          "AbstractJcrBackedUserRoleDao.ERROR_0002_ROLE_NOT_FOUND" ) );
    }
    HashMap<String, User> currentlyAssignedUsers = new HashMap<String, User>();
    Iterator<Authorizable> currentMembers = jackrabbitGroup.getMembers();
    while ( currentMembers.hasNext() ) {
      Authorizable member = currentMembers.next();
      if ( member instanceof User ) {
        currentlyAssignedUsers.put( member.getID(), (User) member );
      }
    }

    HashMap<String, User> finalCollectionOfAssignedUsers = new HashMap<String, User>();
    if ( memberUserNames != null ) {
      ITenant tenant = theTenant == null ? JcrTenantUtils.getTenant( roleName, false ) : theTenant;
      for ( String user : memberUserNames ) {
        User jackrabbitUser = getJackrabbitUser( tenant, user, session );
        if ( jackrabbitUser != null ) {
          finalCollectionOfAssignedUsers.put(
              getTenantedUserNameUtils().getPrincipleId( tenant, user ), jackrabbitUser );
        }
      }
    }

    ArrayList<String> usersToRemove = new ArrayList<String>( currentlyAssignedUsers.keySet() );
    usersToRemove.removeAll( finalCollectionOfAssignedUsers.keySet() );

    ArrayList<String> usersToAdd = new ArrayList<String>( finalCollectionOfAssignedUsers.keySet() );
    usersToAdd.removeAll( currentlyAssignedUsers.keySet() );

    for ( String userId : usersToRemove ) {
      jackrabbitGroup.removeMember( currentlyAssignedUsers.get( userId ) );
      purgeUserFromCache( userId );
    }

    for ( String userId : usersToAdd ) {
      jackrabbitGroup.addMember( finalCollectionOfAssignedUsers.get( userId ) );

      // Purge the UserDetails cache
      purgeUserFromCache( userId );
    }
  }

  private void setUserRolesForNewUser( Session session, final ITenant theTenant, final String userName,
                                       final String[] roles ) throws RepositoryException, NotFoundException {
    Set<String> roleSet = new HashSet<String>();
    if ( roles != null ) {
      roleSet.addAll( Arrays.asList( roles ) );
    }
    roleSet.add( authenticatedRoleName );

    User jackrabbitUser = getJackrabbitUser( theTenant, userName, session );

    if ( ( jackrabbitUser == null )
        || !TenantUtils.isAccessibleTenant( theTenant == null ? tenantedUserNameUtils
        .getTenant( jackrabbitUser.getID() ) : theTenant ) ) {
      throw new NotFoundException( Messages.getInstance().getString(
          "AbstractJcrBackedUserRoleDao.ERROR_0003_USER_NOT_FOUND" ) );
    }

    HashMap<String, Group> finalCollectionOfAssignedGroups = new HashMap<String, Group>();
    ITenant tenant = theTenant == null ? JcrTenantUtils.getTenant( userName, true ) : theTenant;
    for ( String role : roleSet ) {
      Group jackrabbitGroup = getJackrabbitGroup( tenant, role, session );
      if ( jackrabbitGroup != null ) {
        finalCollectionOfAssignedGroups.put( tenantedRoleNameUtils.getPrincipleId( tenant, role ), jackrabbitGroup );
      }
    }

    ArrayList<String> groupsToAdd = new ArrayList<String>( finalCollectionOfAssignedGroups.keySet() );

    for ( String groupId : groupsToAdd ) {
      finalCollectionOfAssignedGroups.get( groupId ).addMember( jackrabbitUser );
      // Purge the UserDetails cache
      purgeUserFromCache( userName );
    }
  }

  private void purgeUserFromCache( String userName ) {
    userDetailsCache.removeUserFromCache( getTenantedUserNameUtils().getPrincipleName( userName ) );
  }

  private boolean oneOfUserIsMySelf( String[] users ) {
    for ( int i = 0; i < users.length; i++ ) {
      if ( isMyself( users[ i ] ) ) {
        return true;
      }
    }
    return false;
  }

  private boolean oneOfUserIsDefaultAdminUser( String[] users ) {
    for ( int i = 0; i < users.length; i++ ) {
      if ( isDefaultAdminUser( users[ i ] ) ) {
        return true;
      }
    }
    return false;
  }

  @VisibleForTesting
  protected boolean isMyself( String userName ) {
    return PentahoSessionHolder.getSession().getName().equals( userName );
  }

  private boolean isDefaultAdminUser( String userName ) {
    String defaultAdminUser =
        PentahoSystem.get( String.class, "singleTenantAdminUserName", PentahoSessionHolder.getSession() );
    if ( defaultAdminUser != null ) {
      return defaultAdminUser.equals( userName );
    }
    return false;
  }

  private boolean adminRoleExist( String[] newRoles ) {
    return Arrays.asList( newRoles ).contains( tenantAdminRoleName );
  }

  public void setUserRoles( Session session, final ITenant theTenant, final String userName, final String[] roles )
      throws RepositoryException, NotFoundException {

    if ( ( isMyself( userName ) || isDefaultAdminUser( userName ) ) && !adminRoleExist( roles ) ) {
      throw new RepositoryException( Messages.getInstance().getString(
          "AbstractJcrBackedUserRoleDao.ERROR_0005_YOURSELF_OR_DEFAULT_ADMIN_USER" ) );
    }

    Set<String> roleSet = new HashSet<String>();
    if ( roles != null ) {
      roleSet.addAll( Arrays.asList( roles ) );
    }
    roleSet.add( authenticatedRoleName );

    User jackrabbitUser = getJackrabbitUser( theTenant, userName, session );

    if ( ( jackrabbitUser == null )
        || !TenantUtils.isAccessibleTenant( theTenant == null ? tenantedUserNameUtils
        .getTenant( jackrabbitUser.getID() ) : theTenant ) ) {
      throw new NotFoundException( Messages.getInstance().getString(
          "AbstractJcrBackedUserRoleDao.ERROR_0003_USER_NOT_FOUND" ) );
    }
    HashMap<String, Group> currentlyAssignedGroups = new HashMap<String, Group>();
    Iterator<Group> currentGroups = jackrabbitUser.memberOf();
    while ( currentGroups.hasNext() ) {
      Group currentGroup = currentGroups.next();
      currentlyAssignedGroups.put( currentGroup.getID(), currentGroup );
    }

    HashMap<String, Group> finalCollectionOfAssignedGroups = new HashMap<String, Group>();
    ITenant tenant = theTenant == null ? JcrTenantUtils.getTenant( userName, true ) : theTenant;
    for ( String role : roleSet ) {
      Group jackrabbitGroup = getJackrabbitGroup( tenant, role, session );
      if ( jackrabbitGroup != null ) {
        finalCollectionOfAssignedGroups.put( tenantedRoleNameUtils.getPrincipleId( tenant, role ), jackrabbitGroup );
      }
    }

    ArrayList<String> groupsToRemove = new ArrayList<String>( currentlyAssignedGroups.keySet() );
    groupsToRemove.removeAll( finalCollectionOfAssignedGroups.keySet() );

    ArrayList<String> groupsToAdd = new ArrayList<String>( finalCollectionOfAssignedGroups.keySet() );
    groupsToAdd.removeAll( currentlyAssignedGroups.keySet() );

    for ( String groupId : groupsToRemove ) {
      currentlyAssignedGroups.get( groupId ).removeMember( jackrabbitUser );
    }

    for ( String groupId : groupsToAdd ) {
      finalCollectionOfAssignedGroups.get( groupId ).addMember( jackrabbitUser );
    }

    // Purge the UserDetails cache
    purgeUserFromCache( userName );
  }

  public IPentahoRole createRole( Session session, final ITenant theTenant, final String roleName,
                                  final String description, final String[] memberUserNames )
      throws AuthorizableExistsException,
      RepositoryException {
    ITenant tenant = theTenant;
    String role = roleName;
    if ( tenant == null ) {
      tenant = JcrTenantUtils.getTenant( roleName, false );
      role = JcrTenantUtils.getPrincipalName( roleName, false );
    }
    if ( tenant == null || tenant.getId() == null ) {
      tenant = JcrTenantUtils.getCurrentTenant();
    }
    if ( !TenantUtils.isAccessibleTenant( tenant ) ) {
      throw new NotFoundException( Messages.getInstance().getString(
          "AbstractJcrBackedUserRoleDao.ERROR_0006_TENANT_NOT_FOUND", theTenant.getId() ) );
    }
    String roleId = tenantedRoleNameUtils.getPrincipleId( tenant, role );

    IPentahoRole pentahoRole = getRole( session, tenant, roleName );
    if ( pentahoRole != null ) {
      throw new AuthorizableExistsException( "Already exists a role for " + roleId );
    }

    UserManager tenantUserMgr = getUserManager( tenant, session );
    // Intermediate path will always be an empty string. The path is already provided while creating a user manager
    tenantUserMgr.createGroup( new PrincipalImpl( roleId ), "" );
    setRoleMembers( session, tenant, role, memberUserNames );
    setRoleDescription( session, tenant, role, description );
    return getRole( session, theTenant, roleName );
  }

  public IPentahoUser createUser( Session session, final ITenant theTenant, final String userName,
                                  final String password, final String description, final String[] roles )
      throws AuthorizableExistsException,
      RepositoryException {
    ITenant tenant = theTenant;
    String user = userName;
    if ( tenant == null ) {
      tenant = JcrTenantUtils.getTenant( userName, true );
      user = JcrTenantUtils.getPrincipalName( userName, true );
    }
    if ( tenant == null || tenant.getId() == null ) {
      tenant = JcrTenantUtils.getCurrentTenant();
    }
    if ( !TenantUtils.isAccessibleTenant( tenant ) ) {
      throw new NotFoundException( Messages.getInstance().getString(
          "AbstractJcrBackedUserRoleDao.ERROR_0006_TENANT_NOT_FOUND", theTenant.getId() ) );
    }
    String userId = tenantedUserNameUtils.getPrincipleId( tenant, user );
    UserManager tenantUserMgr = getUserManager( tenant, session );
    tenantUserMgr.createUser( userId, password, new PrincipalImpl( userId ), "" ); //$NON-NLS-1$
    session.save();
    /**
     * This call is absolutely necessary. setUserRolesForNewUser will never inspect what roles this user is a part of.
     * Since this is a new user it will not be a part of new roles
     **/
    setUserRolesForNewUser( session, tenant, user, roles );
    setUserDescription( session, tenant, user, description );
    session.save();
    createUserHomeFolder( tenant, user, session );
    session.save();
    this.userDetailsCache.removeUserFromCache( userName );
    return getUser( session, tenant, userName );
  }

  public void deleteRole( Session session, final IPentahoRole role ) throws NotFoundException, RepositoryException {
    if ( canDeleteRole( session, role ) ) {
      final List<IPentahoUser> roleMembers = this.getRoleMembers( session, role.getTenant(), role.getName() );
      Group jackrabbitGroup = getJackrabbitGroup( role.getTenant(), role.getName(), session );
      if ( jackrabbitGroup != null
          && TenantUtils.isAccessibleTenant( tenantedRoleNameUtils.getTenant( jackrabbitGroup.getID() ) ) ) {
        jackrabbitGroup.remove();
      } else {
        throw new NotFoundException( "" ); //$NON-NLS-1$
      }
      for ( IPentahoUser roleMember : roleMembers ) {
        purgeUserFromCache( roleMember.getUsername() );
      }
    } else {
      throw new RepositoryException( Messages.getInstance().getString(
          "AbstractJcrBackedUserRoleDao.ERROR_0007_ATTEMPTED_SYSTEM_ROLE_DELETE" ) );
    }
  }

  public void deleteUser( Session session, final IPentahoUser user ) throws NotFoundException, RepositoryException {
    if ( canDeleteUser( session, user ) ) {
      User jackrabbitUser = getJackrabbitUser( user.getTenant(), user.getUsername(), session );
      if ( jackrabbitUser != null
          && TenantUtils.isAccessibleTenant( tenantedUserNameUtils.getTenant( jackrabbitUser.getID() ) ) ) {

        // [BISERVER-9215] Adding new user with same user name as a previously deleted user, defaults to all
        // previous
        // roles
        Iterator<Group> currentGroups = jackrabbitUser.memberOf();
        while ( currentGroups.hasNext() ) {
          currentGroups.next().removeMember( jackrabbitUser );
        }
        getUserCache().remove( jackrabbitUser.getID() );
        purgeUserFromCache( user.getUsername() );
        // [BISERVER-9215]
        jackrabbitUser.remove();
        session.save();
      } else {
        throw new NotFoundException( "" ); //$NON-NLS-1$
      }
    } else {
      throw new RepositoryException( Messages.getInstance().getString(
          "AbstractJcrBackedUserRoleDao.ERROR_0004_LAST_USER_NEEDED_IN_ROLE", tenantAdminRoleName ) );
    }
  }

  public List<IPentahoRole> getRoles( Session session ) throws RepositoryException {
    return getRoles( session, JcrTenantUtils.getCurrentTenant() );
  }

  @VisibleForTesting
  IPentahoUser convertToPentahoUser( User jackrabbitUser ) throws RepositoryException {
    if ( getUserCache().containsKey( jackrabbitUser.getID() ) ) {
      return (IPentahoUser) getUserCache().get( jackrabbitUser.getID() );
    }
    IPentahoUser pentahoUser = null;
    Value[] propertyValues = null;

    String description = null;
    try {
      propertyValues = jackrabbitUser.getProperty( "description" ); //$NON-NLS-1$
      description = propertyValues.length > 0 ? propertyValues[ 0 ].getString() : null;
    } catch ( Exception ex ) {
      // CHECKSTYLES IGNORE
    }

    Credentials credentials = jackrabbitUser.getCredentials();
    String password = null;
    if ( credentials instanceof CryptedSimpleCredentials ) {
      password = new String( ( (CryptedSimpleCredentials) credentials ).getPassword() );
    }

    pentahoUser =
        new PentahoUser( getTenantedUserNameUtils().getTenant( jackrabbitUser.getID() ), getTenantedUserNameUtils()
            .getPrincipleName( jackrabbitUser.getID() ), password, description, !jackrabbitUser.isDisabled() );

    if ( isUseJackrabbitUserCache() ) {
      getUserCache().put( jackrabbitUser.getID(), pentahoUser );
    }

    return pentahoUser;
  }

  private IPentahoRole convertToPentahoRole( Group jackrabbitGroup ) throws RepositoryException {
    IPentahoRole role = null;
    Value[] propertyValues = null;

    String description = null;
    try {
      propertyValues = jackrabbitGroup.getProperty( "description" ); //$NON-NLS-1$
      description = propertyValues.length > 0 ? propertyValues[ 0 ].getString() : null;
    } catch ( Exception ex ) {
      // CHECKSTYLES IGNORE
    }

    role =
        new PentahoRole( tenantedRoleNameUtils.getTenant( jackrabbitGroup.getID() ), tenantedRoleNameUtils
            .getPrincipleName( jackrabbitGroup.getID() ), description );
    return role;
  }

  public List<IPentahoUser> getUsers( Session session ) throws RepositoryException {
    return getUsers( session, JcrTenantUtils.getCurrentTenant() );
  }

  public void setRoleDescription( Session session, final ITenant theTenant, final String roleName,
                                  final String description ) throws NotFoundException, RepositoryException {
    Group jackrabbitGroup = getJackrabbitGroup( theTenant, roleName, session );
    if ( jackrabbitGroup != null
        && TenantUtils.isAccessibleTenant( theTenant == null ? tenantedRoleNameUtils
        .getTenant( jackrabbitGroup.getID() ) : theTenant ) ) {
      if ( description == null ) {
        jackrabbitGroup.removeProperty( "description" ); //$NON-NLS-1$
      } else {
        jackrabbitGroup
            .setProperty( "description", session.getValueFactory().createValue( description ) ); //$NON-NLS-1$
      }
    } else {
      throw new NotFoundException( Messages.getInstance().getString(
          "AbstractJcrBackedUserRoleDao.ERROR_0002_ROLE_NOT_FOUND" ) );
    }
  }

  public void setUserDescription( Session session, final ITenant theTenant, final String userName,
                                  final String description ) throws NotFoundException, RepositoryException {
    User jackrabbitUser = getJackrabbitUser( theTenant, userName, session );
    if ( ( jackrabbitUser == null )
        || !TenantUtils.isAccessibleTenant( theTenant == null ? tenantedUserNameUtils
        .getTenant( jackrabbitUser.getID() ) : theTenant ) ) {
      throw new NotFoundException( Messages.getInstance().getString(
          "AbstractJcrBackedUserRoleDao.ERROR_0003_USER_NOT_FOUND" ) );
    }
    if ( description == null ) {
      jackrabbitUser.removeProperty( "description" ); //$NON-NLS-1$
    } else {
      jackrabbitUser.setProperty( "description", session.getValueFactory().createValue( description ) ); //$NON-NLS-1$
    }
  }

  public void setPassword( Session session, final ITenant theTenant, final String userName, final String password )
      throws NotFoundException, RepositoryException {
    User jackrabbitUser = getJackrabbitUser( theTenant, userName, session );
    if ( ( jackrabbitUser == null )
        || !TenantUtils.isAccessibleTenant( theTenant == null ? tenantedUserNameUtils
        .getTenant( jackrabbitUser.getID() ) : theTenant ) ) {
      throw new NotFoundException( Messages.getInstance().getString(
          "AbstractJcrBackedUserRoleDao.ERROR_0003_USER_NOT_FOUND" ) );
    }
    jackrabbitUser.changePassword( password );

    /**
     * BISERVER-9906 Clear cache after changing password
     */
    purgeUserFromCache( userName );
    userCache.remove( jackrabbitUser.getID() );
  }

  @VisibleForTesting
  protected void setUserDetailsCache( UserCache userDetailsCache ) {
    this.userDetailsCache = userDetailsCache;
  }

  @VisibleForTesting
  protected void setTenantedUserNameUtils( ITenantedPrincipleNameResolver userNameUtils ) {
    tenantedUserNameUtils = userNameUtils;
  }
  public ITenantedPrincipleNameResolver getTenantedUserNameUtils() {
    return tenantedUserNameUtils;
  }

  @VisibleForTesting
  protected void setTenantedRoleNameUtils( ITenantedPrincipleNameResolver roleNameUtils ) {
    tenantedRoleNameUtils = roleNameUtils;
  }

  public ITenantedPrincipleNameResolver getTenantedRoleNameUtils() {
    return tenantedRoleNameUtils;
  }

  public List<IPentahoRole> getRoles( Session session, ITenant tenant ) throws RepositoryException, NamespaceException {
    return getRoles( session, tenant, false );
  }

  public List<IPentahoRole> getRoles( Session session, ITenant theTenant, boolean includeSubtenants )
      throws RepositoryException {
    ArrayList<IPentahoRole> roles = new ArrayList<>();
    if ( theTenant == null || theTenant.getId() == null ) {
      theTenant = JcrTenantUtils.getTenant();
    }

    if ( TenantUtils.isAccessibleTenant( theTenant ) ) {
      UserManager userMgr = getUserManager( theTenant, session );
      pPrincipalName = getJcrName( session );
      Iterator<Authorizable> it = userMgr.findAuthorizables( pPrincipalName, null, UserManager.SEARCH_TYPE_GROUP );
      while ( it.hasNext() ) {
        Group group = (Group) it.next();
        IPentahoRole pentahoRole = convertToPentahoRole( group );
        // Exclude the system role from the list of roles to be returned back
        if ( !extraRoles.contains( pentahoRole.getName() ) ) {
          if ( includeSubtenants ) {
            roles.add( pentahoRole );
          } else {
            if ( pentahoRole.getTenant() != null && pentahoRole.getTenant().equals( theTenant ) ) {
              roles.add( pentahoRole );
            }
          }
        }
      }
    }
    return roles;
  }


  /**
   * Our Sessions should be wrapped in a JDK Dynamic Proxy. Unfortunately some Jackrabbit code is written against the
   * SessionImpl concrete class. This utility unwraps the proxy.
   *
   * @param session
   * @return
   */
  protected static SessionImpl getSessionImpl( Session session ) {
    if ( session instanceof SessionImpl ) {
      return (SessionImpl) session;
    } else if ( Proxy.isProxyClass( session.getClass() ) ) {
      InvocationHandler invocationHandler = Proxy.getInvocationHandler( session );
      if ( invocationHandler instanceof CredentialsStrategySessionFactory.LogoutSuppressingInvocationHandler ) {
        return ( (CredentialsStrategySessionFactory.LogoutSuppressingInvocationHandler) invocationHandler )
            .getSession();
      }
    }
    throw new IllegalStateException( "Session is not a SessionImpl or a proxy of one." );
  }


  private static String getJcrName( Session session ) throws NamespaceException {
    return getSessionImpl( session ).getJCRName( P_PRINCIPAL_NAME );
  }

  public List<IPentahoUser> getUsers( Session session, ITenant tenant ) throws RepositoryException {
    return getUsers( session, tenant, false );
  }

  public List<IPentahoUser> getUsers( Session session, ITenant theTenant, boolean includeSubtenants )
      throws RepositoryException {
    ArrayList<IPentahoUser> users = new ArrayList<IPentahoUser>();
    if ( theTenant == null || theTenant.getId() == null ) {
      theTenant = JcrTenantUtils.getTenant();
    }
    if ( TenantUtils.isAccessibleTenant( theTenant ) ) {
      UserManager userMgr = getUserManager( theTenant, session );
      pPrincipalName = getJcrName( session );
      Iterator<Authorizable> it = userMgr.findAuthorizables( pPrincipalName, null, UserManager.SEARCH_TYPE_USER );
      while ( it.hasNext() ) {
        User user = (User) it.next();
        IPentahoUser pentahoUser = convertToPentahoUser( user );
        if ( includeSubtenants ) {
          users.add( pentahoUser );
        } else {
          if ( pentahoUser.getTenant() != null && pentahoUser.getTenant().equals( theTenant ) ) {
            users.add( pentahoUser );
          }
        }
      }
    }
    return users;
  }

  public IPentahoRole getRole( Session session, final ITenant tenant, final String name ) throws RepositoryException {
    Group jackrabbitGroup = getJackrabbitGroup( tenant, name, session );
    return jackrabbitGroup != null
        && TenantUtils.isAccessibleTenant( tenant == null ? tenantedRoleNameUtils.getTenant( jackrabbitGroup.getID() )
        : tenant ) ? convertToPentahoRole( jackrabbitGroup ) : null;
  }

  private PentahoUserManagerImpl getUserManager( ITenant theTenant, Session session ) throws RepositoryException {
    Properties tenantProperties = new Properties();
    tenantProperties.put( PentahoUserManagerImpl.PARAM_USERS_PATH, PentahoUserManagerImpl.USERS_PATH
        + theTenant.getRootFolderAbsolutePath() );
    tenantProperties.put( PentahoUserManagerImpl.PARAM_GROUPS_PATH, PentahoUserManagerImpl.GROUPS_PATH
        + theTenant.getRootFolderAbsolutePath() );
    return new PentahoUserManagerImpl( getSessionImpl( session ), session.getUserID(), tenantProperties );
  }

  public IPentahoUser getUser( Session session, final ITenant tenant, final String name ) throws RepositoryException {
    User jackrabbitUser = getJackrabbitUser( tenant, name, session );
    return jackrabbitUser != null
        && TenantUtils.isAccessibleTenant( tenant == null ? tenantedUserNameUtils.getTenant( jackrabbitUser.getID() )
        : tenant ) ? convertToPentahoUser( jackrabbitUser ) : null;
  }

  private Group getJackrabbitGroup( ITenant theTenant, String name, Session session ) throws RepositoryException {
    Group jackrabbitGroup = null;
    String roleId = name;
    String roleName = name;
    ITenant tenant = theTenant;

    if ( tenant == null ) {
      tenant = JcrTenantUtils.getTenant( roleName, false );
      roleName = JcrTenantUtils.getPrincipalName( roleName, false );
    }
    if ( tenant == null || tenant.getId() == null ) {
      tenant = JcrTenantUtils.getCurrentTenant();
    }
    if ( tenant == null || tenant.getId() == null ) {
      tenant = JcrTenantUtils.getDefaultTenant();
    }
    roleId = tenantedRoleNameUtils.getPrincipleId( tenant, roleName );

    UserManager userMgr = getUserManager( tenant, session );
    Authorizable authorizable = userMgr.getAuthorizable( roleId );
    if ( authorizable instanceof Group ) {
      jackrabbitGroup = (Group) authorizable;
    }
    return jackrabbitGroup;
  }

  private User getJackrabbitUser( ITenant theTenant, String name, Session session ) throws RepositoryException {
    User jackrabbitUser = null;
    String userId = name;
    String userName = name;
    ITenant tenant = theTenant;
    if ( tenant == null ) {
      tenant = JcrTenantUtils.getTenant( userName, true );
      userName = JcrTenantUtils.getPrincipalName( userName, true );
    }
    if ( tenant == null || tenant.getId() == null ) {
      tenant = JcrTenantUtils.getCurrentTenant();
    }
    if ( tenant == null || tenant.getId() == null ) {
      tenant = JcrTenantUtils.getDefaultTenant();
    }

    if ( tenant != null ) {
      userId = tenantedUserNameUtils.getPrincipleId( tenant, userName );

      UserManager userMgr = getUserManager( tenant, session );
      Authorizable authorizable = userMgr.getAuthorizable( userId );
      if ( authorizable instanceof User ) {
        jackrabbitUser = (User) authorizable;
      }
    }
    return jackrabbitUser;
  }

  protected boolean tenantExists( String tenantName ) {
    return tenantName != null && tenantName.trim().length() > 0;
  }

  public List<IPentahoUser> getRoleMembers( Session session, final ITenant theTenant, final String roleName )
      throws RepositoryException {
    List<IPentahoUser> users = new ArrayList<IPentahoUser>();
    Group jackrabbitGroup = getJackrabbitGroup( theTenant, roleName, session );
    if ( ( jackrabbitGroup != null )
        && TenantUtils.isAccessibleTenant( theTenant == null ? tenantedRoleNameUtils
        .getTenant( jackrabbitGroup.getID() ) : theTenant ) ) {
      Iterator<Authorizable> authorizables = jackrabbitGroup.getMembers();
      while ( authorizables.hasNext() ) {
        Authorizable authorizable = authorizables.next();
        if ( authorizable instanceof User ) {
          users.add( convertToPentahoUser( (User) authorizable ) );
        }
      }
    }
    return users;
  }

  public List<IPentahoRole> getUserRoles( Session session, final ITenant theTenant, final String userName )
      throws RepositoryException {
    ArrayList<IPentahoRole> roles = new ArrayList<IPentahoRole>();
    User jackrabbitUser = getJackrabbitUser( theTenant, userName, session );
    if ( ( jackrabbitUser != null )
        && TenantUtils.isAccessibleTenant( theTenant == null ? tenantedUserNameUtils.getTenant( jackrabbitUser.getID() )
        : theTenant ) ) {
      Iterator<Group> groups = jackrabbitUser.memberOf();
      while ( groups.hasNext() ) {
        IPentahoRole role = convertToPentahoRole( groups.next() );
        // Exclude the extra role from the list of roles to be returned back
        if ( !extraRoles.contains( role.getName() ) ) {
          roles.add( role );
        }
      }
    }
    return roles;
  }

  @VisibleForTesting
  protected RepositoryFile createUserHomeFolder( ITenant theTenant, String username, Session session )
      throws RepositoryException {
    Builder aclsForUserHomeFolder = null;
    Builder aclsForTenantHomeFolder = null;

    if ( theTenant == null ) {
      theTenant = JcrTenantUtils.getTenant( username, true );
      username = JcrTenantUtils.getPrincipalName( username, true );
    }
    if ( theTenant == null || theTenant.getId() == null ) {
      theTenant = JcrTenantUtils.getCurrentTenant();
    }
    if ( theTenant == null || theTenant.getId() == null ) {
      theTenant = JcrTenantUtils.getDefaultTenant();
    }
    RepositoryFile userHomeFolder = null;
    String userId = tenantedUserNameUtils.getPrincipleId( theTenant, username );
    final RepositoryFileSid userSid = new RepositoryFileSid( userId );
    RepositoryFile tenantHomeFolder = null;
    RepositoryFile tenantRootFolder = null;
    RepositoryFileSid ownerSid = null;
    // Get the Tenant Root folder. If the Tenant Root folder does not exist then exit.
    tenantRootFolder =
        JcrRepositoryFileUtils.getFileByAbsolutePath( session, ServerRepositoryPaths
            .getTenantRootFolderPath( theTenant ), pathConversionHelper, lockHelper, false, null );
    if ( tenantRootFolder != null ) {
      // Try to see if Tenant Home folder exist
      tenantHomeFolder =
          JcrRepositoryFileUtils.getFileByAbsolutePath( session, ServerRepositoryPaths
              .getTenantHomeFolderPath( theTenant ), pathConversionHelper, lockHelper, false, null );

      if ( tenantHomeFolder == null ) {
        String ownerId = tenantedUserNameUtils.getPrincipleId( theTenant, username );
        ownerSid = new RepositoryFileSid( ownerId, Type.USER );

        String tenantAuthenticatedRoleId = tenantedRoleNameUtils.getPrincipleId( theTenant, authenticatedRoleName );
        RepositoryFileSid tenantAuthenticatedRoleSid = new RepositoryFileSid( tenantAuthenticatedRoleId, Type.ROLE );

        aclsForTenantHomeFolder =
            new RepositoryFileAcl.Builder( userSid ).ace( tenantAuthenticatedRoleSid, EnumSet
                .of( RepositoryFilePermission.READ ) );

        aclsForUserHomeFolder =
            new RepositoryFileAcl.Builder( userSid ).ace( ownerSid, EnumSet.of( RepositoryFilePermission.ALL ) );
        tenantHomeFolder =
            internalCreateFolder( session, tenantRootFolder.getId(), new RepositoryFile.Builder( ServerRepositoryPaths
                    .getTenantHomeFolderName() ).folder( true ).title(
                Messages.getInstance().getString( "AbstractJcrBackedUserRoleDao.usersFolderDisplayName" ) ).build(),
                aclsForTenantHomeFolder.build(), "tenant home folder" ); //$NON-NLS-1$
      } else {
        String ownerId = tenantedUserNameUtils.getPrincipleId( theTenant, username );
        ownerSid = new RepositoryFileSid( ownerId, Type.USER );
        aclsForUserHomeFolder =
            new RepositoryFileAcl.Builder( userSid ).ace( ownerSid, EnumSet.of( RepositoryFilePermission.ALL ) );
      }

      // now check if user's home folder exist
      userHomeFolder =
          JcrRepositoryFileUtils.getFileByAbsolutePath( session, ServerRepositoryPaths.getUserHomeFolderPath(
              theTenant, username ), pathConversionHelper, lockHelper, false, null );
      if ( userHomeFolder == null ) {
        RepositoryFile.Builder newFolder = new RepositoryFile.Builder( username ).folder( true );
        String hidePropertyValue = PentahoSystem.get( ISystemConfig.class )
          .getProperty( PentahoSystem.HIDE_USER_HOME_FOLDER_ON_CREATION_PROPERTY );
        Boolean hideUserHomeFolder = hidePropertyValue != null && "true".equals( hidePropertyValue.toLowerCase() );
        newFolder = newFolder.hidden( hideUserHomeFolder );
        userHomeFolder =
            internalCreateFolder( session, tenantHomeFolder.getId(), newFolder.build(),
              aclsForUserHomeFolder.build(), "user home folder" ); //$NON-NLS-1$
      }

    }
    return userHomeFolder;
  }

  private RepositoryFile internalCreateFolder( final Session session, final Serializable parentFolderId,
                                               final RepositoryFile folder, final RepositoryFileAcl acl,
                                               final String versionMessage )
      throws RepositoryException {
    PentahoJcrConstants pentahoJcrConstants = new PentahoJcrConstants( session );
    JcrRepositoryFileUtils.checkoutNearestVersionableFileIfNecessary( session, pentahoJcrConstants, parentFolderId );
    Node folderNode = JcrRepositoryFileUtils.createFolderNode( session, pentahoJcrConstants, parentFolderId, folder );
    // we must create the acl during checkout
    JcrRepositoryFileAclUtils.createAcl( session, pentahoJcrConstants, folderNode.getIdentifier(), acl == null
        ? defaultAclHandler.createDefaultAcl( folder ) : acl );
    session.save();
    if ( folder.isVersioned() ) {
      JcrRepositoryFileUtils.checkinNearestVersionableNodeIfNecessary( session, pentahoJcrConstants, folderNode,
          versionMessage );
    }
    JcrRepositoryFileUtils.checkinNearestVersionableFileIfNecessary( session, pentahoJcrConstants, parentFolderId,
        Messages.getInstance().getString( "JcrRepositoryFileDao.USER_0001_VER_COMMENT_ADD_FOLDER", folder.getName(),
            ( parentFolderId == null ? "root" : parentFolderId.toString() ) ) ); //$NON-NLS-1$ //$NON-NLS-2$
    return JcrRepositoryFileUtils.nodeToFile( session, pentahoJcrConstants, pathConversionHelper, lockHelper,
        folderNode );
  }

  /**
   * Checks to see if the removal of the received roles and users would cause the system to have no login associated
   * with the Admin role. This check is to be made before any changes take place
   *
   * @return Error message if invalid or null if ok
   * @throws RepositoryException
   */
  @VisibleForTesting
  protected boolean canDeleteUser( Session session, final IPentahoUser user ) throws RepositoryException {
    boolean userHasAdminRole = false;
    List<IPentahoRole> roles = getUserRoles( null, user.getUsername() );
    for ( IPentahoRole role : roles ) {
      if ( tenantAdminRoleName.equals( role.getName() ) ) {
        userHasAdminRole = true;
        break;
      }
    }

    if ( ( isMyself( user.getUsername() ) || isDefaultAdminUser( user.getUsername() ) ) && userHasAdminRole ) {
      throw new RepositoryException( Messages.getInstance().getString(
          "AbstractJcrBackedUserRoleDao.ERROR_0008_UNABLE_TO_DELETE_USER_IS_YOURSELF_OR_DEFAULT_ADMIN_USER" ) );
    }

    if ( userHasAdminRole ) {
      List<IPentahoUser> usersWithAdminRole = getRoleMembers( session, null, tenantAdminRoleName );
      if ( usersWithAdminRole == null ) {
        throw new RepositoryException( Messages.getInstance().getString(
            "AbstractJcrBackedUserRoleDao.ERROR_0004_LAST_USER_NEEDED_IN_ROLE", tenantAdminRoleName ) );
      }
      if ( usersWithAdminRole.size() > 1 ) {
        return true;
      } else if ( usersWithAdminRole.size() == 1 ) {
        return false;
      } else {
        throw new RepositoryException( Messages.getInstance().getString(
            "AbstractJcrBackedUserRoleDao.ERROR_0004_LAST_USER_NEEDED_IN_ROLE", tenantAdminRoleName ) );
      }
    }
    return true;
  }

  private boolean canDeleteRole( Session session, final IPentahoRole role ) {
    return !( role != null && systemRoles.contains( role.getName() ) );
  }

  private String[] findRemovedUsers( List<IPentahoUser> savedUsers, String[] toBeSaved ) {
    List<String> usersToBeRemoved = new ArrayList<String>();
    List<String> toBeSavedUsers = Arrays.asList( toBeSaved );
    for ( int i = 0; i < savedUsers.size(); i++ ) {
      if ( toBeSavedUsers != null && toBeSaved.length > 0 ) {
        if ( !toBeSavedUsers.contains( savedUsers.get( i ).getUsername() ) ) {
          usersToBeRemoved.add( savedUsers.get( i ).getUsername() );
        }
      } else {
        usersToBeRemoved.add( savedUsers.get( i ).getUsername() );
      }
    }
    return usersToBeRemoved.toArray( new String[ 0 ] );
  }

  public boolean isUseJackrabbitUserCache() {
    return useJackrabbitUserCache;
  }

  public void setUseJackrabbitUserCache( boolean useJackrabbitUserCache ) {
    this.useJackrabbitUserCache = useJackrabbitUserCache;
  }

  @VisibleForTesting
  protected void initUserCache() {
    userCache = new LRUMap( 4096 );
  }

  @VisibleForTesting
  protected LRUMap getUserCache() {
    return userCache;
  }

  @VisibleForTesting
  protected void initUserDetailsCache() {
    userDetailsCache = new NullUserCache();
  }
}
